#!/usr/bin/env python
# coding: utf-8

# ## TMDB 5000 Movie Dataset

# 三种推荐系统baseline:-
# 
# > *  **Demographic Filtering**- 人口统计过滤，根据电影流行度或类型为每个用户提供广义推荐。系统向具有相似人口特征的用户推荐相同的电影。 由于每个用户的情况不同，这种方式被认为过于简单。该系统背后的基本思想是，更受欢迎和广受好评的电影将有更高的概率被普通观众喜欢。
# 
# 

# > *  **Content Based Filtering**- 基于内容的推荐，根据物品或内容的元数据，发现物品或内容的相关性，然后基于用户以前的喜好记录推荐给用户相似的物品。该系统使用item的元数据，例如电影的流派、导演、描述、演员等，来做出这些推荐。 这些推荐系统背后的一般思想是，如果一个人喜欢某个特定item，他或她也会喜欢与其相似的item。

# > *  **Collaborative Filtering**- 协同过滤，该系统匹配具有相似兴趣的人，并基于此匹配提供推荐。协作过滤器不需要像基于内容的对应物那样的项目元数据。

# ### Load data

# In[2]:


# 将文件夹里的zip文件解压
import pandas as pd 
import numpy as np 
df1=pd.read_csv('./tmdb_5000_credits.csv')
df2=pd.read_csv('./tmdb_5000_movies.csv')


# The first dataset contains the following features:-
# 
# * movie_id - A unique identifier for each movie.
# * cast - The name of lead and supporting actors.
# * crew - The name of Director, Editor, Composer, Writer etc.
# 
# The second dataset has the following features:- 
# 
# * budget - The budget in which the movie was made.
# * genre - The genre of the movie, Action, Comedy ,Thriller etc.
# * homepage - A link to the homepage of the movie.
# * id - This is infact the movie_id as in the first dataset.
# * keywords - The keywords or tags related to the movie.
# * original_language - The language in which the movie was made.
# * original_title - The title of the movie before translation or adaptation.
# * overview - A brief description of the movie.
# * popularity - A numeric quantity specifying the movie popularity.
# * production_companies - The production house of the movie.
# * production_countries - The country in which it was produced.
# * release_date - The date on which it was released.
# * revenue - The worldwide revenue generated by the movie.
# * runtime - The running time of the movie in minutes.
# * status - "Released" or "Rumored".
# * tagline - Movie's tagline.
# * title - Title of the movie.
# * vote_average -  average ratings the movie recieved.
# * vote_count - the count of votes recieved.
# 
# join the two dataset on the 'id'
# 

# In[3]:


df1.columns = ['id','tittle','cast','crew']
df2= df2.merge(df1,on='id')


# In[4]:


df2.head(5)


# # **Demographic Filtering** -
#    Before getting started with this  -
# * we need a metric to score or rate movie 
# * Calculate the score for every movie 
# * Sort the scores and recommend the best rated movie to the users.
# 
# We can use the average ratings of the movie as the score but using this won't be fair enough since a movie with 8.9 average rating and only 3 votes cannot be considered better than the movie with 7.8 as as average rating but 40 votes.
# So, I'll be using IMDB's weighted rating (wr) which is given as :-
# 
# ![](./wr.png)
# where,
# * v is the number of votes for the movie;
# * m is the minimum votes required to be listed in the chart;
# * R is the average rating of the movie; And
# * C is the mean vote across the whole report
# 
# We already have v(**vote_count**) and R (**vote_average**) and C can be calculated as 

# In[5]:


C= df2['vote_average'].mean()
C


# 在10分制下，所有电影的平均评分约为6分。下一步是确定m的值，m是图表中列出的最低票数。我们将使用第90百分位作为我们的分界点。换言之，一部电影要在排行榜上占据一席之地，它的得票率必须高于至少90%的上榜电影。

# In[6]:


m= df2['vote_count'].quantile(0.9)
m


# 筛选出符合要求的电影

# In[7]:


q_movies = df2.copy().loc[df2['vote_count'] >= m]
q_movies.shape


# 有481部电影符合条件。现在需要计算每部合格电影的指标。我们将定义一个函数**weighted_rating（）**并定义一个新列**score**

# In[8]:


def weighted_rating(x, m=m, C=C):
    v = x['vote_count']
    R = x['vote_average']
    # Calculation based on the IMDB formula
    return (v/(v+m) * R) + (m/(m+v) * C)


# In[9]:


# Define a new feature 'score' and calculate its value with `weighted_rating()`
q_movies['score'] = q_movies.apply(weighted_rating, axis=1)


# 根据分数进行排序，并输出前10部电影的标题、投票计数、投票平均数和加权评分数。

# In[10]:


q_movies = q_movies.sort_values('score', ascending=False)

#Print the top 15 movies
q_movies[['title', 'vote_count', 'vote_average', 'score']].head(10)


# In[12]:


pop= df2.sort_values('popularity', ascending=False)
import matplotlib.pyplot as plt
plt.figure(figsize=(12,4))

plt.barh(pop['title'].head(6),pop['popularity'].head(6), align='center',
        color='skyblue')
plt.gca().invert_yaxis()
plt.xlabel("Popularity")
plt.title("Popular Movies")


# 它为所有用户提供了一个推荐电影的总览。他们对特定用户的兴趣和品味不敏感。我们将转向更精细的系统-基于内容的过滤。

# # **Content Based Filtering**
# 
# ![](./conten.png)

# ## **Plot description based Recommender**
# 我们两两计算每部电影的描述词的相似度'overview'字段，根据相似度评分给到推荐结果

# In[13]:


df2['overview'].head(5)


# ### TfIdfVectorizer

# In[14]:


#Import TfIdfVectorizer from scikit-learn
from sklearn.feature_extraction.text import TfidfVectorizer

#Define a TF-IDF Vectorizer Object. Remove all english stop words such as 'the', 'a'
tfidf = TfidfVectorizer(stop_words='english')

#Replace NaN with an empty string
df2['overview'] = df2['overview'].fillna('')

#Construct the required TF-IDF matrix by fitting and transforming the data
tfidf_matrix = tfidf.fit_transform(df2['overview'])

#Output the shape of tfidf_matrix
tfidf_matrix.shape


# 在我们的数据集中，有超过20000个不同的词用来描述4800部电影。
# 
# 有了这个矩阵，我们现在可以计算相似性分数了。这方面有几个候选方法；例如欧几里德、皮尔逊和余弦相似性分数，哪个分数最好没有正确答案。不同的分数在不同的场景下效果很好，用不同的标准进行实验通常是个好主意。
# 
# 我们将使用余弦相似性来计算表示两部电影之间相似性的数字量。
# ![]()

# **linear_kernel()** instead of cosine_similarities() 

# In[15]:


# Import linear_kernel
from sklearn.metrics.pairwise import linear_kernel

# Compute the cosine similarity matrix
cosine_sim = linear_kernel(tfidf_matrix, tfidf_matrix)


# 我们定义一个函数，该函数接收电影标题作为输入，并输出10个最相似电影的列表。
# 首先，我们需要电影标题和索引的反向映射。换句话说，我们需要一种机制来识别源数据中给定电影标题的电影索引。

# In[16]:


#Construct a reverse map of indices and movie titles
indices = pd.Series(df2.index, index=df2['title']).drop_duplicates()


# 我们现在可以很好地定义我们的推荐函数。步骤：-
# 
# *获取电影标题的索引。
# 
# *获取特定电影与所有电影的余弦相似性分数列表。将其转换为元组列表，其中第一个元素是其位置，第二个元素是相似性分数。
# 
# *根据相似度得分对上述元组列表进行排序；
# 
# *获取此列表的前10个元素。忽略第一个元素，因为它指的是self（最类似于特定电影的电影是电影本身）。
# 
# *返回与顶级元素的索引相对应的标题。

# In[17]:


# 输入一个电影标题，输出相似度最高的top 10电影作为推荐
def get_recommendations(title, cosine_sim=cosine_sim):
    # Get the index of the movie that matches the title
    idx = indices[title]

    # Get the pairwsie similarity scores of all movies with that movie
    sim_scores = list(enumerate(cosine_sim[idx]))

    # Sort the movies based on the similarity scores
    sim_scores = sorted(sim_scores, key=lambda x: x[1], reverse=True)

    # Get the scores of the 10 most similar movies
    sim_scores = sim_scores[1:11]

    # Get the movie indices
    movie_indices = [i[0] for i in sim_scores]

    # Return the top 10 most similar movies
    return df2['title'].iloc[movie_indices]


# In[18]:


get_recommendations('The Dark Knight Rises')


# In[19]:


get_recommendations('The Avengers')


# ### But: 这样的推荐系统虽然在寻找情节描述相似的电影方面做得不错，但推荐的质量并不是那么好。”黑暗骑士崛起“返回所有蝙蝠侠电影，喜欢这部电影的人更想看其他克里斯托弗诺兰电影。这是当前系统无法捕捉到的。

# ## **Credits, Genres and Keywords Based Recommender**
# 我们需要构建更细粒度的数据，这样推荐的质量将会提高。这正是我们本节要做的。基于以下数据构建一个推荐器：3个顶级演员、导演、相关类型和电影情节关键字。
# 
# 从演员、剧组和关键词特征中，我们需要提取三个最重要的演员、导演和与该电影相关的关键词。现在，我们的数据是以“字符串化”列表的形式呈现的，我们需要将其转换为安全且可用的结构。

# In[21]:


df2.loc[0,'cast']


# In[22]:


df2.loc[0,'crew']


# In[23]:


df2.loc[0,'keywords']


# In[24]:


df2.loc[0,'genres']


# In[25]:


# 将文件中的str转为list
# 'cast'演员阵容，'crew'摄制组信息，'keyword'电影关键词，'genres'电影流派、类型信息
from ast import literal_eval

features = ['cast', 'crew', 'keywords', 'genres']
for feature in features:
    df2[feature] = df2[feature].apply(literal_eval)


# 在这些字段中提取出我们需要的特征

# In[26]:


# 在 crew：摄制组信息字段里，提取导演信息，如果没有该信息返回NaN
def get_director(x):
    for i in x:
        if i['job'] == 'Director':
            return i['name']
    return np.nan


# In[27]:


# 提取三名演员信息
def get_list(x):
    if isinstance(x, list):
        names = [i['name'] for i in x]
        #Check if more than 3 elements exist. If yes, return only first three. If no, return entire list.
        if len(names) > 3:
            names = names[:3]
        return names

    # 未提取成功时，返回空list
    return []


# In[28]:


# df中增加提取到的特征
df2['director'] = df2['crew'].apply(get_director)

features = ['cast', 'keywords', 'genres']
for feature in features:
    df2[feature] = df2[feature].apply(get_list)


# In[29]:


# 新特征
df2[['title', 'cast', 'director', 'keywords', 'genres']].head(3)


# In[30]:


# 将名称和关键字实例转换为小写，并去掉它们之间的所有空格。
def clean_data(x):
    if isinstance(x, list):
        return [str.lower(i.replace(" ", "")) for i in x]
    else:
        #Check if director exists. If not, return empty string
        if isinstance(x, str):
            return str.lower(x.replace(" ", ""))
        else:
            return ''


# In[31]:


# 数据清洗
features = ['cast', 'keywords', 'director', 'genres']

for feature in features:
    df2[feature] = df2[feature].apply(clean_data)


# 现在可以创建我们的"metadata soup"，它是一个字符串，包含我们想要提供给向量器的所有元数据（即actors、director和关键字）。

# In[32]:


def create_soup(x):
    return ' '.join(x['keywords']) + ' ' + ' '.join(x['cast']) + ' ' + x['director'] + ' ' + ' '.join(x['genres'])
df2['soup'] = df2.apply(create_soup, axis=1)


# 我们使用CountVectorizer（）而不是TF-IDF。这是因为如果一个演员/导演在相对较多的电影中表演或执导，我们不能降低他的分量。

# In[33]:


# Import CountVectorizer and create the count matrix
from sklearn.feature_extraction.text import CountVectorizer

count = CountVectorizer(stop_words='english')
count_matrix = count.fit_transform(df2['soup'])


# In[34]:


# Compute the Cosine Similarity matrix based on the count_matrix
from sklearn.metrics.pairwise import cosine_similarity

cosine_sim2 = cosine_similarity(count_matrix, count_matrix)


# In[35]:


# Reset index of our main DataFrame and construct reverse mapping as before
df2 = df2.reset_index()
indices = pd.Series(df2.index, index=df2['title'])


# In[36]:


get_recommendations('The Dark Knight Rises', cosine_sim2)


# In[37]:


get_recommendations('The Godfather', cosine_sim2)


# 可以看到我们的推荐由于更多的元数据而成功地捕获了更多的信息，给出了更多元化的推荐。
# 惊奇漫画或DC漫画迷更可能喜欢同一家制片厂的电影。因此，我们可以在上面的功能中添加*production company*。
# 
# 优点：数据更加丰富

# # **Collaborative Filtering - base on SVD**

# 用SVD实现协同过滤。
# 
# 我们之前使用的数据集没有userId（在协同过滤是必需的），所以我们使用另一个数据集。
# 
# using the [**Surprise** ](https://surprise.readthedocs.io/en/stable/index.html) library to implement SVD.

# In[40]:


import pandas as pd
from surprise import Reader, Dataset, SVD
from surprise.model_selection import cross_validate
reader = Reader()
ratings = pd.read_csv('./ratings_small.csv')
ratings.head()


# 在这个数据集中，电影的评分是5分，与之前的不同。包含不同用户对电影的打分情况。

# In[43]:


len(ratings)


# In[44]:


data = Dataset.load_from_df(ratings[['userId', 'movieId', 'rating']], reader)
svd = SVD()
cross_validate(svd, data, measures=['RMSE', 'MAE'], cv=5)


# 我们得到的平均根平均平方误差约为0.89，这对于我们的情况已经足够了。现在让我们对数据集进行训练并得出预测。

# In[45]:


trainset = data.build_full_trainset()
svd.fit(trainset)


# pick user with user Id 1  and check the ratings she/he has given.

# In[46]:


ratings[ratings['userId'] == 1]


# In[47]:


svd.predict(1, 1172, 4)


# In[48]:


svd.predict(1, 31, 2.5)

